//Copyright 2017 Huawei Technologies Co., Ltd
//
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.

// Package lager is the package for lager
package log

import (
	"archive/zip"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"time"
)

var pathReplacer *strings.Replacer

// EscapPath escape path
func EscapPath(msg string) string {
	if pathReplacer == nil {
		return msg
	}
	return pathReplacer.Replace(msg)
}

func removeFile(path string) error {
	fileInfo, err := os.Stat(path)
	if err != nil {
		return err
	}
	if fileInfo.IsDir() {
		return nil
	}
	err = os.Remove(path)
	if err != nil {
		return err
	}
	return nil
}

func removeExceededFiles(path string, baseFileName string,
	maxKeptCount int, rotateStage string) {
	if maxKeptCount < 0 {
		return
	}
	var pat string
	switch rotateStage {
	case "rollover":
		//rotated file, svc.log.20060102150405000
		pat = fmt.Sprintf(`%s\.[0-9]{1,17}$`, regexp.QuoteMeta(baseFileName))
	case "backup":
		//backup compressed file, svc.log.20060102150405000.zip
		pat = fmt.Sprintf(`%s\.[0-9]{17}\.zip$`, regexp.QuoteMeta(baseFileName))
	default:
		return
	}
	fileRegexp, err := regexp.Compile(pat)
	if err != nil {
		Logger.Errorf(err, "regexp.Compile: %s failed.", pat)
		return
	}
	fileList, err := FilterFileList(path, fileRegexp)
	if err != nil {
		Logger.Errorf(err, "filepath.Walk() path: %s failed.", EscapPath(path))
		return
	}
	if len(fileList) <= maxKeptCount {
		return
	}
	sort.Strings(fileList)
	//remove exceeded files, keep file count below maxBackupCount
	for len(fileList) > maxKeptCount {
		filePath := fileList[0]
		err := removeFile(filePath)
		if err != nil {
			Logger.Errorf(err, "remove filePath: %s failed.", EscapPath(filePath))
			break
		}
		//remove the first element of a list
		fileList = append(fileList[:0], fileList[1:]...)
	}
}

//filePath: file full path, like ${_APP_LOG_DIR}/svc.log.1
//fileBaseName: rollover file base name, like svc.log
//replaceTimestamp: whether or not to replace the num. of a rolled file
func compressFile(filePath, fileBaseName string, replaceTimestamp bool) error {
	ifp, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer ifp.Close()

	var zipFilePath string
	if replaceTimestamp {
		//svc.log.1 -> svc.log.20060102150405000.zip
		zipFileBase := fileBaseName + "." + getTimeStamp() + "." + "zip"
		zipFilePath = filepath.Dir(filePath) + "/" + zipFileBase
	} else {
		zipFilePath = filePath + ".zip"
	}
	zipFile, err := os.OpenFile(zipFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0440)
	if err != nil {
		return err
	}
	defer zipFile.Close()

	zipWriter := zip.NewWriter(zipFile)
	defer zipWriter.Close()

	ofp, err := zipWriter.Create(filepath.Base(filePath))
	if err != nil {
		return err
	}

	_, err = io.Copy(ofp, ifp)
	if err != nil {
		return err
	}

	return nil
}

func shouldRollover(fPath string, MaxFileSize int) bool {
	if MaxFileSize < 0 {
		return false
	}

	fileInfo, err := os.Stat(fPath)
	if err != nil {
		Logger.Errorf(err, "state path: %s failed.", EscapPath(fPath))
		return false
	}

	if fileInfo.Size() > int64(MaxFileSize*1024*1024) {
		return true
	}
	return false
}

func doRollover(fPath string, MaxFileSize int, MaxBackupCount int) {
	if !shouldRollover(fPath, MaxFileSize) {
		return
	}

	timeStamp := getTimeStamp()
	//absolute path
	rotateFile := fPath + "." + timeStamp
	err := CopyFile(fPath, rotateFile)
	if err != nil {
		Logger.Errorf(err, "copy path: %s failed.", EscapPath(fPath))
	}

	//truncate the file
	f, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
	if err != nil {
		Logger.Errorf(err, "truncate path: %s failed.", EscapPath(fPath))
		return
	}
	f.Close()

	//remove exceeded rotate files
	removeExceededFiles(filepath.Dir(fPath), filepath.Base(fPath), MaxBackupCount, "rollover")
}

func doBackup(fPath string, MaxBackupCount int) {
	if MaxBackupCount <= 0 {
		return
	}
	baseName := filepath.Base(fPath)
	quoted := regexp.QuoteMeta(baseName)
	pat := fmt.Sprintf(`%s\.[0-9]{1,17}$`, quoted)
	fileRegexp, err := regexp.Compile(pat)
	if err != nil {
		Logger.Errorf(err, "regexp.Compile: %s failed.", pat)
		return
	}
	p := fmt.Sprintf(`%s\.[0-9]{17}$`, quoted)
	stampedfileRegexp, err := regexp.Compile(p)
	if err != nil {
		Logger.Errorf(err, "regexp.Compile: %s failed.", p)
		return
	}
	fDir := filepath.Dir(fPath)
	err = WalkFile(fDir, fileRegexp, func(file string) error {
		var err error
		if stampedfileRegexp.MatchString(file) {
			//svc.log.20060102150405000, not replace Timestamp
			err = compressFile(file, filepath.Base(fPath), false)
		} else {
			//svc.log.1, replace Timestamp
			err = compressFile(file, filepath.Base(fPath), true)
		}
		if err != nil {
			Logger.Errorf(err, "compress path: %s failed.", EscapPath(file))
			return err
		}
		err = removeFile(file)
		if err != nil {
			Logger.Errorf(err, "remove path %s failed.", EscapPath(file))
		}
		return nil
	}, nil)
	if err != nil {
		Logger.Errorf(err, "walk path: %s failed.", EscapPath(fDir))
		return
	}

	//remove exceeded backup files
	removeExceededFiles(fDir, baseName, MaxBackupCount, "backup")
}

func logRotateFile(file string, MaxFileSize int, MaxBackupCount int) {
	defer func() {
		if e := recover(); e != nil {
			Logger.Errorf(nil, "LogRotate file path: %s catch an exception.", EscapPath(file))
		}
	}()

	doRollover(file, MaxFileSize, MaxBackupCount)
	doBackup(file, MaxBackupCount)
}

var logExtRegexp = regexp.MustCompile(`.(\.log|\.trace|\.out)$`)

// LogRotate function for log rotate
// path:			where log files need rollover
// MaxFileSize: 		MaxSize of a file before rotate. By M Bytes.
// MaxBackupCount: 	Max counts to keep of a log's backup files.
func LogRotate(path string, MaxFileSize int, MaxBackupCount int) {
	//filter .log .trace files
	defer func() {
		if e := recover(); e != nil {
			err, _ := e.(error)
			Logger.Errorf(err, "LogRotate catch an exception")
		}
	}()

	err := WalkFile(path, logExtRegexp, func(file string) error {
		logRotateFile(file, MaxFileSize, MaxBackupCount)
		return nil
	}, nil)
	if err != nil {
		Logger.Errorf(err, "filepath.Walk() path: %s failed.", path)
		return
	}
}

// FilterFileList function for filter file list
//path : where the file will be filtered
//pat  : regexp pattern to filter the matched file
func FilterFileList(path string, fileRegexp *regexp.Regexp) ([]string, error) {
	capacity := 10
	//initialize a fileName slice, len=0, cap=10
	fileList := make([]string, 0, capacity)
	err := filepath.Walk(path,
		func(pathName string, f os.FileInfo, e error) error {
			if e != nil {
				return e
			}
			if f.IsDir() {
				return nil
			}
			if fileRegexp != nil && !fileRegexp.MatchString(f.Name()) {
				return nil
			}
			fileList = append(fileList, pathName)
			return nil
		})
	return fileList, err
}

func WalkFile(path string, fileRegexp *regexp.Regexp, onMatched func(string) error, onUnmatched func(string) error) error {
	return filepath.Walk(path,
		func(pathName string, f os.FileInfo, e error) error {
			if e != nil {
				return e
			}
			if f.IsDir() {
				return nil
			}
			if fileRegexp.MatchString(f.Name()) {
				if onMatched == nil {
					return nil
				}
				return onMatched(pathName)
			}
			if onUnmatched == nil {
				return nil
			}
			return onUnmatched(pathName)
		})
}

// getTimeStamp get time stamp
func getTimeStamp() string {
	return strings.Replace(time.Now().Format("20060102150405.000"), `.`, ``, 1)
}

// CopyFile copy file
func CopyFile(srcFile, destFile string) error {
	file, err := os.Open(srcFile)
	if err != nil {
		return err
	}
	defer file.Close()
	dest, err := os.Create(destFile)
	if err != nil {
		return err
	}
	defer dest.Close()
	_, err = io.Copy(dest, file)
	return err
}

// initLogRotate initialize log rotate
func initLogRotate(logFilePath string, c *Lager) {
	if logFilePath == "" {
		return
	}
	if c == nil {
		go func() {
			for {
				LogRotate(filepath.Dir(logFilePath), LogRotateSize, LogBackupCount)
				time.Sleep(30 * time.Second)
			}
		}()
		return
	}
	c.fixed()
	if c.RollingPolicy == RollingPolicySize {
		go func() {
			for {
				LogRotate(filepath.Dir(logFilePath), c.LogRotateSize, c.LogBackupCount)
				time.Sleep(30 * time.Second)
			}
		}()
		return
	}
	go func() {
		for {
			LogRotate(filepath.Dir(logFilePath), 0, c.LogBackupCount)
			time.Sleep(24 * time.Hour * time.Duration(c.LogRotateDate))
		}
	}()
}
